package nl.ViewerServer.ServerManager.DataManager;

import java.io.File;
import java.io.IOException;
import java.sql.SQLException;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.Iterator;

import nl.ViewerServer.Application;
import nl.ViewerServer.Database.DataManagerDatabaseConnection;
import nl.ViewerServer.Database.Objects.ParameterValue;
import nl.ViewerServer.Exceptions.DataInitializationException;
import nl.ViewerServer.Exceptions.DataObjectException;
import nl.ViewerServer.Exceptions.UserRequestException;
import nl.ViewerServer.ServerManager.ServerManager;
import nl.ViewerServer.ServerManager.DataManager.DataObjects.DataObject;
import nl.ViewerServer.ServerManager.DataManager.DataObjects.ImageDataObject;
import nl.ViewerServer.ServerManager.DataManager.DataObjects.ZoomifyDataObject;
import nl.ViewerServer.ServerManager.DataManager.Objects.Dataset;
import nl.ViewerServer.ServerManager.DataManager.Objects.Lookmark;
import nl.ViewerServer.ServerManager.DataManager.Objects.LookmarkInfo;
import nl.ViewerServer.ServerManager.DataManager.Objects.Modality;
import nl.ViewerServer.ServerManager.DataManager.Objects.Output;
import nl.ViewerServer.ServerManager.DataManager.Objects.Parameter;
import nl.ViewerServer.ServerManager.DataManager.Objects.Visualization;
import nl.ViewerServer.ServerManager.DataManager.Util.MD5;
import nl.ViewerServer.ServerManager.UserManager.Objects.User;
import nl.ViewerServer.SessionManager.Objects.Job;
import nl.ViewerServer.SessionManager.Objects.Session;

import org.apache.log4j.Logger;

public class DataManager {
   private static Logger                      log                    = Logger.getLogger( DataManager.class );

   protected HashMap<Integer, Dataset>        datasets               = new HashMap<Integer, Dataset>();
   protected HashMap<Modality, Visualization> modalityVisualizations = new HashMap<Modality, Visualization>();
   private DataManagerDatabaseConnection      database;
   private HashMap<Integer, DataObject>       dataObjectCache        = new HashMap<Integer, DataObject>();
   private HashMap<Integer, DataObject>       lookmarkCache          = new HashMap<Integer, DataObject>();

   private HashMap<Integer, Visualization> visualizations = new HashMap<Integer, Visualization>();
   
   // TODO: cleanup cache when needed

   public HashMap<Integer, Dataset> getDatasets() {
      return datasets;
   }

   /**
    * This method will return an arraylist containing all datasets which are visible to the specified user.
    * 
    * @param au
    * @return an arraylist containing all datasets which are visible to the specified user
    */
   public ArrayList<Dataset> getDatasets( User au ) throws SQLException {
      ArrayList<Dataset> datasetsAllowed = new ArrayList<Dataset>();

      // go through all datasets and modalities and select those that are visible to the specified user
      for( Dataset ds : datasets.values() ) {
         if( ServerManager.userManager.userHasPrivilege( ds.getDatasetACL(), "SELECT", au ) ) {
            Dataset clonedDataset = ds.clone();
            datasetsAllowed.add( clonedDataset );
            for( Modality m : ds.getModalities().values() ) {
               if( ServerManager.userManager.userHasPrivilege( m.getModalityACL(), "SELECT", au ) ) {
                  Modality clonedModality = m.clone();
                  clonedDataset.addModality( clonedModality );
                  for( Visualization v : m.getVisualizations().values() ) {
                     if( ServerManager.userManager.userHasPrivilege( v.getVisualizationACL(), "SELECT", au ) ) {
                        clonedModality.addVisualization( v );
                     }
                  }
               }
            }
         }
      }

      return datasetsAllowed;
   }

   public DataManager() throws Exception {
      // our own private database connection
      database = new DataManagerDatabaseConnection();
   }

   public Modality getModality( int modalityId ) {
      Iterator<Dataset> datasetIterator = datasets.values().iterator();
      while( datasetIterator.hasNext() ) {
         Dataset ds = datasetIterator.next();
         Modality m = ds.getModality( modalityId );
         if( m != null )
            return m;
      }
      return null;
   }

   public Modality addModality( int datasetId, String datasetName, String datasetDescription, String datasetACL,
         int modalityId, String modalityName, String modalityDescription, int sourceId, String modalityACL ) {

      // check if the dataset exists
      Dataset ds = null;
      if( ( ds = datasets.get( datasetId ) ) == null ) {
         // create new dataset

         ds = new Dataset( datasetId, datasetName, datasetDescription, datasetACL );
         datasets.put( ds.datasetId, ds );
      }

      // create modality and add it to dataset
      if( ds.getModality( modalityId ) != null ) {
         log.warn( "Modality is already present in Dataset object, skipping" );
         return ds.getModality( modalityId );
      }

      Modality m = new Modality( modalityId, modalityName, modalityDescription, datasetId, sourceId, modalityACL );

      ds.addModality( m );
      log.debug( "Modality '" + modalityName + "'added to dataset" );
      return m;
   }

   public boolean addModalityAlignment( int modalityId1, int modalityId2 ) {
      Modality m1 = getModality( modalityId1 );
      if( m1 == null ) {
         log.warn( "Modality " + modalityId1 + " for alignment does not exist" );
         return false;
      }
      Modality m2 = getModality( modalityId2 );
      if( m2 == null ) {
         log.warn( "Modality " + modalityId2 + " for alignment does not exist" );
         return false;
      }

      m1.addAlignment( m2 );
      return true;
   }

   public Visualization getVisualization( int visualizationId ) {
      return visualizations.get( visualizationId );
   }

   public Visualization addVisualizationAndOutputs( int visualizationId, String visualizationName,
         String visualizationType, String visualizationACL, boolean requiresAlignedSources, int outputId, String outputName, String outputDataType ) {

      // check for the representation
      Visualization v = getVisualization( visualizationId );

      if( v == null ) {
         // create representation
         v = new Visualization( visualizationId, visualizationName, visualizationType, visualizationACL, requiresAlignedSources );
         log.info( "Visualization " + visualizationId + " created" );

         visualizations.put( v.visualizationId, v );
         v.addOutput( new Output( outputId, outputName, outputDataType ) );
         return v;
      }

      Output output = new Output( outputId, outputName, outputDataType );
      v.addOutput( output );
      log.info( "Output " + outputId + " created" );
      return v;
   }
   
   public synchronized String getStoreLocation( long jobId ) throws IOException {
      File storeFolder = new File( Application.config.dataStore );

      log.debug( "Free space on data store '" + storeFolder.getCanonicalPath() + "': " + storeFolder.getUsableSpace() );
      // long currentTime = (System.currentTimeMillis()/1000) - 1233499000;

      // linux filesystem (under ext3) can contain at most 32000 files
      // make sure to create two levels of directories (to accomodate locations for up to
      // 3200000 job results)

      String dirName = ( jobId % 10 ) + "/" + ( ( jobId / 10 ) % 10 ) + "/" + jobId;
      File newDir = new File( storeFolder, dirName );
      int count = 0;
      while( newDir.exists() ) {
         if( count == 100 )
            throw new IOException( "No store location could be found" );

         dirName = "" + jobId + "." + count++;
         newDir = new File( storeFolder, dirName );

      }

      if( !newDir.mkdirs() ) {
         throw new IOException( "Directory " + newDir + " could not be created, is writing allowed?" );
      }

      return newDir.getCanonicalPath();
   }

   /**
    * Method to remote a store location directory. This method will recursively delete the storeLocation path
    * 
    * @param storeLocation
    */
   public synchronized void withdrawStoreLocation( String storeLocation ) {
      // remove the store location folder with all contents
      File location = new File( storeLocation );
      deleteDirectory( location );
   }

   /**
    * Private method to recursively remove a directory
    * 
    * @param path
    * @return
    */
   private static boolean deleteDirectory( File path ) {
      if( path.exists() ) {
         File[] files = path.listFiles();
         for( int i = 0; i < files.length; i++ ) {
            if( files[i].isDirectory() ) {
               deleteDirectory( files[i] );
            } else {
               files[i].delete();
            }
         }
      }
      return( path.delete() );
   }

   private void cacheDataObject( DataObject object ) {
      dataObjectCache.put( object.getDataObjectId(), object );
   }

   private void cacheLookmark( int lookmarkId, DataObject object ) {
      lookmarkCache.put( lookmarkId, object );
   }

   /**
    * The storeOutput method is responsible for creating a lookmark with dataobject from a specified job and session.
    * Both the lookmark and dataobject will be cached.
    * 
    * @param job
    * @param session
    * @return the lookmarkId of the lookmark that was created
    * @throws SQLException
    * @throws DataInitializationException
    */

   public synchronized int storeOutput( Job job, Session session ) throws SQLException, DataInitializationException {
      // create a lookmark record with all parameter value records
      Lookmark lookmark = database.createLookmarkFromSession( job, session );

      // create a data record for the actual output
      DataObject dataObject = createDataObject( session.getSelectedOutput().getOutputDataType() );
      dataObject.setLookmarkId( lookmark.lookmarkId );
      dataObject.setLocation( (String)session.getParameter( "StoreLocation", null ).getParameterValue() );
      dataObject.initialize();

      int dataObjectId = database.addDataObject( dataObject );
      log.debug( "DataObject created: " + dataObjectId + " (" + dataObject + ")" );

      cacheDataObject( dataObject );

      cacheLookmark( lookmark.lookmarkId, dataObject );
      log.info( "Lookmark created: " + lookmark.lookmarkId );
      return lookmark.lookmarkId;
   }

   public static DataObject createDataObject( String dataType ) {
      if( dataType != null ) {
         if( dataType.equalsIgnoreCase( "image" ) ) {
            return new ImageDataObject( dataType );
         } else if( dataType.equalsIgnoreCase( "zoomify" ) ) {
            return new ZoomifyDataObject( dataType );
         }
      }
      // we don't know what to do, don't return a data object
      return null;
   }

   /**
    * Method to create a fingerprint of the parameters used to setup the pipeline
    * 
    * @param parameters
    * @return
    */

   public static String getParameterFingerprint( HashMap<String, Parameter> parameters ) {
      // get the parameter, sort them in alphabetical order
      Collection<Parameter> parameterCollection = parameters.values();
      ArrayList<Parameter> paramList = new ArrayList<Parameter>( parameterCollection );
      Collections.sort( paramList );

      StringBuffer concat = new StringBuffer();

      for( Parameter p : paramList ) {
         if( concat.length() > 0 ) {
            concat.append( "\n" );
         }
         concat.append( p.parameterName );
         concat.append( "=" );
         concat.append( p.getPythonRepresentation() );
      }
      /*
       * java.security.MessageDigest md5 = MessageDigest.getInstance("MD5"); md5.digest(concat.toString().getBytes());
       */
      MD5 md5 = new MD5( concat );

      return md5.asHex();
   }

   /**
    * Method used to prepare a dataobject to be accessed through the webservice
    * 
    * @param dataObjectId
    * @return
    * @throws SQLException
    * @throws DataObjectException
    */

   public DataObject getDataObject( int dataObjectId ) throws SQLException, DataInitializationException {
      DataObject dataObject;
      dataObject = dataObjectCache.get( dataObjectId );
      if( dataObject != null ) {
         log.debug( "DataObject " + dataObjectId + "fetched from cache" );
         return dataObject;
      }

      dataObject = database.getDataObject( dataObjectId );
      if( dataObject == null ) {
         log.debug( "DataObject with id " + dataObjectId + " not found" );
         return null;
      }
      dataObject.initialize();

      log.debug( "DataObject fetched from database" );
      cacheDataObject( dataObject );
      return dataObject;
   }

   /**
    * Method used to get the dataobject that is associated with a given lookmark
    * 
    * @param lookmarkId
    * @return
    * @throws SQLException
    * @throws DataObjectException
    *            when the database object can not be initialized after being loaded from the database
    */

   public DataObject getDataObjectForLookmark( int lookmarkId ) throws SQLException, DataInitializationException {
      DataObject dataObject;
      dataObject = lookmarkCache.get( lookmarkId );
      if( dataObject != null ) {
         log.debug( "Lookmark " + lookmarkId + " dataObject fetched from lookmark cache" );
         return dataObject;
      }

      dataObject = database.getDataObjectForLookmark( lookmarkId );
      if( dataObject == null ) {
         // this can be called for every job, ignore the lower-than-debug printout
         // log.debug("Lookmark DataObject with lookmark id "+lookmarkId+" not found");
         return null;
      }
      dataObject.initialize();

      log.debug( "DataObject for lookmark " + lookmarkId + " fetched from database" );
      cacheLookmark( lookmarkId, dataObject );
      return dataObject;
   }

   // /**
   // *
   // * Try to translate from a given job and session to a lookmark (which was stored in the database) If a lookmark was
   // * found try to get the dataobject associated with it
   // *
   // * @param job
   // * @param session
   // * @return
   // * @throws SQLException
   // * @throws DataObjectException
   // */
   //
   // public DataObject findDataObjectForLookmark( Job job, Session session ) throws SQLException,
   // DataInitializationException {
   // String fingerprint = getParameterFingerprint( session.getAllVisualizationParameters() );
   //
   // int lookmarkId = database.findLookmark( session, fingerprint );
   //
   // if( lookmarkId < 0 ) {
   // return null;
   // }
   //
   // DataObject lookmarkDataObject = fetchDataObjectForLookmark( lookmarkId );
   //
   // return lookmarkDataObject;
   // }

   public DataObject getDataObjectForJob( User user, long jobId ) throws UserRequestException, SQLException,
         DataInitializationException {
      int lookmarkAndUserId[] = database.findLookmarkForJob( jobId );
      if( lookmarkAndUserId == null || lookmarkAndUserId.length != 2 )
         return null;

      if( lookmarkAndUserId[1] != user.userId )
         throw new UserRequestException( "Cannot retrieve lookmark for other users" );

      return getDataObjectForLookmark( lookmarkAndUserId[0] );
   }

   public ArrayList<ParameterValue> getParameterValuesForJob( User user, long jobId ) throws UserRequestException,
         SQLException, DataInitializationException {
      int lookmarkAndUserId[] = database.findLookmarkForJob( jobId );
      if( lookmarkAndUserId == null || lookmarkAndUserId.length != 2 )
         return null;

      if( lookmarkAndUserId[1] != user.userId )
         throw new UserRequestException( "Cannot retrieve lookmark for other users" );

      return database.getUserParameterValuesForLookmark( lookmarkAndUserId[0] );
   }

   /**
    * Alter an existing lookmark in the database to set it user defined (instead of system defined) and add a
    * description
    * 
    * 
    * @param user
    * @param jobId
    * @param isPersonal
    * @param description
    * @return true when the lookmark was created successfull, false otherwise
    * @throws UserRequestException
    * @throws SQLException
    */
   public boolean createUserDefinedLookmark( User user, long jobId, boolean isPersonal, String description )
         throws UserRequestException, SQLException {
      int lookmarkAndUserId[] = database.findLookmarkForJob( jobId );

      if( lookmarkAndUserId == null || lookmarkAndUserId.length != 2 )
         return false;

      if( lookmarkAndUserId[1] != user.userId )
         throw new UserRequestException( "Cannot alter lookmark for other users" );

      // now ask the database to change this system-defined lookmark into a user-defined one
      database.createUserDefinedLookmark( lookmarkAndUserId[0], isPersonal, description );

      return true;
   }

   public ArrayList<LookmarkInfo> getLookmarks( User user, int datasetId ) throws SQLException {
      ArrayList<LookmarkInfo> lookmarks = database.getLookmarks( user.userId, datasetId );

      // check for each visualization, modality and dataset for the elements of the list of lookmarks
      // whether the user is allowed to access the object (btw, this could take some time for large amounts of
      // lookmarks)
      for( int i = 0; i < lookmarks.size(); i++ ) {
         LookmarkInfo li = lookmarks.get( i );
         if( !ServerManager.userManager.userHasPrivilege( li.datasetACL, "SELECT", user ) || !ServerManager.userManager.userHasPrivilege(
               li.modalityACL, "SELECT", user ) || !ServerManager.userManager.userHasPrivilege( li.visualizationACL,
               "SELECT", user ) ) {
            lookmarks.remove( li );
         }
      }

      return lookmarks;
   }

   /**
    * This method will fetch information on a lookmark
    * 
    * @param lookmarkId
    * @return int array of length three containing the userId, modalityId and the visualizationId.
    * @throws SQLException
    */
   public int[] getIdsForLookmark( int lookmarkId ) throws SQLException, UserRequestException {
      int[] modalityAndVisualizationId = database.getIdsForLookmark( lookmarkId );

      if( modalityAndVisualizationId == null )
         throw new UserRequestException( "lookmarkId specified was not found" );

      return modalityAndVisualizationId;
   }

   public ArrayList<ParameterValue> getParameterValuesForLookmark( User user, int lookmarkUserId, int lookmarkId )
         throws UserRequestException, SQLException, DataInitializationException {

      if( lookmarkUserId != user.userId )
         throw new UserRequestException( "Cannot retrieve lookmark for other users" );

      return database.getUserParameterValuesForLookmark( lookmarkId );
   }
}
